<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>大文件上传-断点续传</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.js"></script>
    <!-- 
        大文件上传
           - 分片上传
           - 断点续传
     -->
</head>

<body>
    <div>
        <input type="file" id="chooseFile">
        <button id="uploadBtn">上传</button>
    </div>
    <div style="margin: 20px 0;">
        <progress value="0" id="progress"></progress>
        <span id="message"></span>
    </div>
    <div>
        <button id="stopUpload">暂停</button>
        <button id="keepUpload">续传</button>
    </div>

    <script>
        const chooseFile = document.getElementById('chooseFile');
        const uploadBtn = document.getElementById('uploadBtn');
        const progress = document.getElementById('progress');
        const message = document.getElementById('message');
        const stopUpload = document.getElementById('stopUpload');
        const keepUpload = document.getElementById('keepUpload');

        let checkedFile = null; // 用户选中的文件

        const chunkSize = 10 * 1024 * 1024; // 每一个分片的大小：10M
        let chunksList = [];  // 切片文件, 全局目的，断点续传使用

        const CancelToken = axios.CancelToken;
        let source = CancelToken.source();

        // 选择上传文件
        chooseFile.addEventListener('change', function (e) {
            const [file] = e.target.files;
            if (file) {
                checkedFile = file;
            }
        })

        /**
         * 上传文件入口
         *
         */
        uploadBtn.addEventListener('click', async function () {
            // 如果没有选中的文件，直接结束
            if (!checkedFile) return;

            // 验证文件之前是否上传成功过,文件已存在返回"上传成功（秒传）"
            const res = await verifyFile();
            if (res.code) {
                message.innerText = "上传成功（秒传）";
                progress.value = progress.max;   // 将文件最大值设置为进度条的 value 值
                clearTip()
                return;   // 终止后续代码的执行
            }
            // 文件切片
            createChunks();
            // 上传切片
            uploadChunks(chunksList, res.uploaded);
        })

        // 暂停上传
        stopUpload.addEventListener('click', function () {
            // 取消后续所有“切片上传”请求的发送
            source.cancel('终止上传！');
            // 重置 source
            source = CancelToken.source();
            message.innerText = "暂停上传";
        })

        // 点击续传按钮
        keepUpload.addEventListener('click', async function () {
            const res = await verifyFile();
            if (!res.code) {
                // 只要没有上传成功，不管是否有之前的上传记录，都需要继续上传
                uploadChunks(chunksList, res.uploaded);  // 将已上传的部分数据传递过去
            }
        })

        /** 
         * 文件切片
         * checkedFile:File  选中大文件
         * chunksList:Array  存储切片文件
         */
        const createChunks = () => {
            let start = 0;   // 切片文件的起始位置
            while (start < checkedFile.size) {
                // 文件切片
                const chunkItem = checkedFile.slice(start, start + chunkSize);
                // 将切片保存到全局的切片数组中
                chunksList.push(chunkItem);
                start += chunkSize;
            }
            // 将文件切片的总数作为进度条的最大值
            progress.max = chunksList.length;
        }

        /** 
         * 上传切片文件
         * chunksList:Array 存储切片
         * uploaded: Array 已经上传切片，断点续传
         */
        const uploadChunks = async (chunksList = [], uploaded = []) => {
            // 将之前已上传的切片数量设置为当前进度条的进度
            progress.value = uploaded.length;
            // 处理切片数据格式
            let formDataList = chunksList.map((chunkItem, index) => {
                let formData = new FormData();
                formData.append('file', chunkItem);   // 切片文件信息
                formData.append('fileName', checkedFile.name); // 完整文件名
                formData.append('chunkName', index);  // 切片名(将 index 作为每一个切片的名字)
                return formData;
            });
            // 将处理好的文件切片 formData 数据中，还未上传的部分筛选出来
            formDataList = formDataList.filter((_, index) => uploaded.indexOf(index + '') < 0);
            // 依次上传每一个切片文件
            const requestList = formDataList.map(formData => {
                return axios({
                    url: 'http://localhost:3000/upload',
                    data: formData,
                    method: 'POST',
                    cancelToken: source.token,   // 添加 cancelToken，用于后续取消请求发送
                }).then(() => {
                    // 每一个切片上传成功后，进度条都 +1
                    progress.value++;
                })

            });
            // 等待所有切片上传完成
            await Promise.all(requestList);
            mergeChunks(); // 调用合并文件方法
        }

        /**
         * 清除提示
         */
        const clearTip = () => {
            setTimeout(() => {
                message.innerText = "";
                progress.value = 0;
            }, 2000);
        }

        // 合并切片
        const mergeChunks = async () => {
            const { data } = await axios({
                url: 'http://localhost:3000/merge',
                method: 'POST',
                data: {
                    chunkSize,
                    fileName: checkedFile.name
                }
            });
            if (data.code) {
                message.innerText = '文件上传成功';
                clearTip()
            }
        }

        // 验证文件是否上传过
        const verifyFile = async () => {
            const { data } = await axios({
                url: 'http://localhost:3000/verify',
                method: 'POST',
                data: {
                    fileName: checkedFile.name
                }
            });
            return data;
        }



    </script>

</body>

</html>